diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index b2fd7980f947bc3cbd76c6bb9a1b6a0465ffe959..980dd7eab3edc5416d91b3e199f6324251db0cd0 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -9,8 +9,8 @@ from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget
 from dynamic_preferences.forms import PreferenceForm
 from material import Fieldset, Layout, Row
 
-from .mixins import ExtensibleForm
-from .models import Announcement, Group, GroupType, Person
+from .mixins import ExtensibleForm, SchoolYearRelatedExtensibleForm
+from .models import Announcement, Group, GroupType, Person, SchoolYear
 from .registries import (
     group_preferences_registry,
     person_preferences_registry,
@@ -289,3 +289,13 @@ class EditGroupTypeForm(forms.ModelForm):
     class Meta:
         model = GroupType
         exclude = []
+
+
+class SchoolYearForm(ExtensibleForm):
+    """Form for managing school years."""
+
+    layout = Layout("name", Row("date_start", "date_end"))
+
+    class Meta:
+        model = SchoolYear
+        exclude = []
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index 4767a8f3492c7401a22164fe25f54da4ec9fd702..2657cc18105ce918e327b1c29e4e21937720d76d 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -82,6 +82,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("School years"),
+                    "url": "school_years",
+                    "icon": "date_range",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_schoolyear",
+                        ),
+                    ],
+                },
                 {
                     "name": _("Data management"),
                     "url": "data_management",
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 3325f136af03b52deb9be44f765cc82e2e832b70..2cbcad73fb3544a52d23da2fdd5254ce612589c5 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -219,3 +219,13 @@ view_group_type_predicate = has_person & (
     has_global_perm("core.view_grouptype") | has_any_object("core.view_grouptype", GroupType)
 )
 add_perm("core.view_grouptype", view_group_type_predicate)
+
+# School years
+view_school_year_predicate = has_person & has_global_perm("core.view_schoolyear")
+add_perm("core.view_schoolyear", view_school_year_predicate)
+
+create_school_year_predicate = has_person & has_global_perm("core.add_schoolyear")
+add_perm("core.create_schoolyear", create_school_year_predicate)
+
+edit_school_year_predicate = has_person & has_global_perm("core.change_schoolyear")
+add_perm("core.edit_schoolyear", edit_school_year_predicate)
diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py
index ff8fd871e75e9b97453784928d65456c5f857e8b..b6c8f3a157aa31e19808d86a703abfdb387e81b4 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -4,6 +4,24 @@ import django_tables2 as tables
 from django_tables2.utils import A
 
 
+class SchoolYearTable(tables.Table):
+    """Table to list persons."""
+
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    name = tables.LinkColumn("edit_school_year", args=[A("id")])
+    date_start = tables.Column()
+    date_end = tables.Column()
+    edit = tables.LinkColumn(
+        "edit_school_year",
+        args=[A("id")],
+        text=_("Edit"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}},
+        verbose_name=_("Actions"),
+    )
+
+
 class PersonsTable(tables.Table):
     """Table to list persons."""
 
diff --git a/aleksis/core/templates/core/school_year/create.html b/aleksis/core/templates/core/school_year/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..65ad9e7efa181b4a57cf9d797ce5da84aec687b1
--- /dev/null
+++ b/aleksis/core/templates/core/school_year/create.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Manage school year{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Manage school year{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/school_year/edit.html b/aleksis/core/templates/core/school_year/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..123a033a2d162c9b7e828de060ebcc5a746ca855
--- /dev/null
+++ b/aleksis/core/templates/core/school_year/edit.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit school year{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit school year{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/school_year/list.html b/aleksis/core/templates/core/school_year/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..5a432e91019c00b3a351ff68a29efa81d4f0ae36
--- /dev/null
+++ b/aleksis/core/templates/core/school_year/list.html
@@ -0,0 +1,18 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}School years{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}School years{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a class="btn green waves-effect waves-light" href="{% url 'create_school_year' %}">
+    <i class="material-icons left">add</i>
+    {% trans "Create school year" %}
+  </a>
+
+  {% render_table table %}
+{% endblock %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 47cb1d63c85c41b562e54909d553febac99c7625..7d3c9135465573de377e1903f0e1c5be890a42f2 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -21,6 +21,9 @@ urlpatterns = [
     path("status/", views.system_status, name="system_status"),
     path("", include(tf_urls)),
     path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
+    path("school_years/", views.SchoolYearListView.as_view(), name="school_years"),
+    path("school_years/create/", views.SchoolYearCreateView.as_view(), name="create_school_year"),
+    path("school_years/<int:pk>/", views.SchoolYearEditView.as_view(), name="edit_school_year"),
     path("persons", views.persons, name="persons"),
     path("persons/accounts", views.persons_accounts, name="persons_accounts"),
     path("person", views.person, name="person"),
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index b5415c45d293218ac64d7003c905350ea02eb3ad..7687a161fd0b45818e98f8accc886d58cc03bbf8 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -2,20 +2,20 @@ from typing import Optional
 
 from django.apps import apps
 from django.conf import settings
-from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.core.exceptions import PermissionDenied
 from django.core.paginator import Paginator
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
 
-from django_tables2 import RequestConfig
+from django_tables2 import RequestConfig, SingleTableView
 from dynamic_preferences.forms import preference_form_builder
 from guardian.shortcuts import get_objects_for_user
 from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
 from haystack.views import SearchView
-from rules.contrib.views import permission_required
+from rules.contrib.views import PermissionRequiredMixin, permission_required
 
 from .filters import GroupFilter
 from .forms import (
@@ -27,15 +27,25 @@ from .forms import (
     GroupPreferenceForm,
     PersonPreferenceForm,
     PersonsAccountsFormSet,
+    SchoolYearForm,
     SitePreferenceForm,
 )
-from .models import Announcement, DashboardWidget, Group, GroupType, Notification, Person
+from .mixins import AdvancedCreateView, AdvancedEditView
+from .models import (
+    Announcement,
+    DashboardWidget,
+    Group,
+    GroupType,
+    Notification,
+    Person,
+    SchoolYear,
+)
 from .registries import (
     group_preferences_registry,
     person_preferences_registry,
     site_preferences_registry,
 )
-from .tables import GroupsTable, GroupTypesTable, PersonsTable
+from .tables import GroupsTable, GroupTypesTable, PersonsTable, SchoolYearTable
 from .util import messages
 from .util.apps import AppConfig
 from .util.core_helpers import objectgetter_optional
@@ -82,6 +92,37 @@ def about(request: HttpRequest) -> HttpResponse:
     return render(request, "core/about.html", context)
 
 
+class SchoolYearListView(SingleTableView, PermissionRequiredMixin):
+    """Table of all school years."""
+
+    model = SchoolYear
+    table_class = SchoolYearTable
+    permission_required = "core.view_schoolyear"
+    template_name = "core/school_year/list.html"
+
+
+class SchoolYearCreateView(AdvancedCreateView, PermissionRequiredMixin):
+    """Create view for school years."""
+
+    model = SchoolYear
+    form_class = SchoolYearForm
+    permission_required = "core.add_schoolyear"
+    template_name = "core/school_year/create.html"
+    success_url = reverse_lazy("school_years")
+    success_message = _("The school year has been created.")
+
+
+class SchoolYearEditView(AdvancedEditView, PermissionRequiredMixin):
+    """Edit view for school years."""
+
+    model = SchoolYear
+    form_class = SchoolYearForm
+    permission_required = "core.edit_schoolyear"
+    template_name = "core/school_year/edit.html"
+    success_url = reverse_lazy("school_years")
+    success_message = _("The school year has been saved.")
+
+
 @permission_required("core.view_persons")
 def persons(request: HttpRequest) -> HttpResponse:
     """List view listing all persons."""