diff --git a/aleksis/core/static/public/style.scss b/aleksis/core/static/public/style.scss
index 26ac19ca5d715923f279e9eb07b8e172110aa4bf..889b790f98cb0be7ed2de37a12a884acff5c4ca9 100644
--- a/aleksis/core/static/public/style.scss
+++ b/aleksis/core/static/public/style.scss
@@ -23,6 +23,9 @@ rect#background {
 .waves-effect.waves-primary .waves-ripple {
   background-color: lighten($primary-color, 5%);
 }
+.waves-effect.waves-secondary .waves-ripple {
+  background-color: lighten($secondary-color, 5%);
+}
 
 .success {
   @extend .light-green, .lighten-3
@@ -974,3 +977,16 @@ svg.iconify {
     text-align: center;
   }
 }
+
+.btn-small-line-height {
+  line-height: $button-small-height;
+}
+
+.btn-smaller-padding {
+  padding: 0 8px;
+}
+
+p.ical-description {
+  margin: 0;
+  font-weight: 300;
+}
diff --git a/aleksis/core/templates/core/ical/ical_create.html b/aleksis/core/templates/core/ical/ical_create.html
new file mode 100644
index 0000000000000000000000000000000000000000..64ff80083ebab3e61cd108d46ee1bff158cdd236
--- /dev/null
+++ b/aleksis/core/templates/core/ical/ical_create.html
@@ -0,0 +1,20 @@
+{% extends 'core/base.html' %}
+{% load i18n material_form %}
+
+{% block page_title %}{% blocktrans %}Create iCal URL{% endblocktrans %}{% endblock page_title %}
+{% block browser_title %}{% blocktrans %}Create iCal URL{% endblocktrans %}{% endblock browser_title %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+
+    {% form form=form %}{% endform %}
+
+    {% include "core/partials/save_button.html" %}
+    <a href="{% url "ical_feed_list" %}" class="btn red">
+      <i class="material-icons iconify left" data-icon="mdi:close"></i>
+      {% blocktrans %}Cancel{% endblocktrans %}
+    </a>
+  </form>
+
+{% endblock content %}
diff --git a/aleksis/core/templates/core/ical/ical_edit.html b/aleksis/core/templates/core/ical/ical_edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..cd46c475531bc1154f5f36c16ae1476b79e73137
--- /dev/null
+++ b/aleksis/core/templates/core/ical/ical_edit.html
@@ -0,0 +1,20 @@
+{% extends 'core/base.html' %}
+{% load i18n material_form %}
+
+{% block page_title %}{% blocktrans %}Edit iCal URL {{ object }}{% endblocktrans %}{% endblock page_title %}
+{% block browser_title %}{% blocktrans %}Edit iCal URL {{ object }}{% endblocktrans %}{% endblock browser_title %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+
+    {% form form=form %}{% endform %}
+
+    {% include "core/partials/save_button.html" %}
+    <a href="{% url "ical_feed_list" %}" class="btn red">
+      <i class="material-icons iconify left" data-icon="mdi:close"></i>
+      {% blocktrans %}Cancel{% endblocktrans %}
+    </a>
+  </form>
+
+{% endblock content %}
diff --git a/aleksis/core/templates/core/ical/ical_list.html b/aleksis/core/templates/core/ical/ical_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..abd3d9b25ea0ba2f85aac6281f73ede48f4c1cb5
--- /dev/null
+++ b/aleksis/core/templates/core/ical/ical_list.html
@@ -0,0 +1,39 @@
+{% extends 'core/base.html' %}
+{% load i18n msg_box static %}
+
+{% block page_title %}{% trans "ICal Feeds" %}{% endblock page_title %}
+{% block browser_title %}{% trans "ICal Feeds" %}{% endblock browser_title %}
+
+{% block content %}
+  {% trans "These are URLs for different Calendar Feeds in the iCal (.ics) format. You can create as many as you want and import them in your calendar software." as msg %}
+  {% msg_box msg=msg status="info" %}
+  <a href="{% url "ical_feed_create" %}" class="btn green">
+  <i class="material-icons iconify left" data-icon="mdi:add"></i>
+  {% trans "Create iCal URL" %}
+  </a>
+  <h2>{% trans "Your iCal URLs" %}</h2>
+  <div class="collection">
+    {% for object in object_list %}
+      <div class="collection-item">
+        <span class="title btn-small-line-height">
+          {{ object }}
+          <a href="{% url "ical_feed_delete" object.pk %}"
+                  class="secondary-content btn-flat btn-small red-text btn-smaller-padding waves-effect waves-red">
+            <i class="material-icons iconify" data-icon="mdi:delete-outline"></i>
+          </a>
+          <a href="{% url "ical_feed_edit" object.pk %}"
+             class="secondary-content btn-flat btn-small primary-color-text btn-smaller-padding waves-effect waves-primary">
+            <i class="material-icons iconify" data-icon="mdi:pencil-outline"></i>
+          </a>
+          <button type="button" data-target="input-{{ forloop.counter0 }}"
+                  class="secondary-content btn-flat btn-small secondary-color-text btn-smaller-padding copy-button waves-effect waves-secondary">
+            <i class="material-icons iconify" data-icon="mdi:content-copy"></i>
+          </button>
+        </span>
+        <p class="ical-description">{{ object.ical_feed_object.title }} – {{ object.ical_feed_object.description }}</p>
+        <input type="url" readonly value="https://{{ request.site }}{{ object.get_absolute_url }}"
+               id="input-{{ forloop.counter0 }}">
+      </div>
+    {% endfor %}
+  </div>
+{% endblock content %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index f39808e0ef0ed22e9c3b94e91fc5c1161beccdb9..58a8c29d4b60b448b6fd05a5f7f83c732565d159 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -320,6 +320,13 @@ urlpatterns = [
         name="assign_permission",
     ),
     path("pdfs/<int:pk>/", views.RedirectToPDFFile.as_view(), name="redirect_to_pdf_file"),
+
+    path("ical/", views.ICalFeedListView.as_view(), name="ical_feed_list"),
+    path("ical/create/", views.ICalFeedCreateView.as_view(), name="ical_feed_create"),
+    path("ical/<int:pk>/edit/", views.ICalFeedEditView.as_view(), name="ical_feed_edit"),
+    path("ical/<int:pk>/delete/", views.ICalFeedDeleteView.as_view(), name="ical_feed_delete"),
+    path("ical/<slug:slug>.ics", views.ICalFeedView.as_view(), name="ical_feed"),
+
     path("__icons__/", include("dj_iconify.urls")),
 ]
 
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 32e83c3746630ddf96e5c179180dbfb2face50d2..36ef779b84b17af8cccffdf5a1dce614b6837f1b 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -105,6 +105,7 @@ from .models import (
     PersonInvitation,
     SchoolTerm,
     TaskUserAssignment,
+    PersonalICalUrl,
 )
 from .registries import (
     group_preferences_registry,
@@ -1564,3 +1565,67 @@ class CustomAuthorizationView(AuthorizationView):
         context = super().get_context_data(**kwargs)
         context["no_menu"] = True
         return context
+
+
+class ICalFeedView(DetailView):
+    model = PersonalICalUrl
+    slug_field = "uuid"
+
+    def get(self, request, *args, **kwargs):
+        obj: PersonalICalUrl = self.get_object()
+        if obj.ical_feed_object:
+            kwargs["person"] = obj.person
+            return obj.ical_feed_object()(request, *args, **kwargs)
+        else:
+            return HttpResponse(status=204)
+
+
+class ICalFeedListView(ListView):
+    model = PersonalICalUrl
+    template_name = "core/ical/ical_list.html"
+
+    def get_queryset(self):
+        return PersonalICalUrl.objects.filter(person=self.request.user.person)
+
+
+class ICalFeedEditView(AdvancedEditView):
+    model = PersonalICalUrl
+    template_name = "core/ical/ical_edit.html"
+    success_url = reverse_lazy("ical_feed_list")
+    success_message = _("ICal feed updated successfully")
+
+    fields = ["name", "ical_feed"]
+
+    def get_queryset(self):
+        return PersonalICalUrl.objects.filter(person=self.request.user.person)
+
+    def form_valid(self, form):
+        obj = form.save(commit=False)
+        obj.person = self.request.user.person
+        obj.save()
+        return super().form_valid(form)
+
+
+class ICalFeedDeleteView(AdvancedDeleteView):
+    model = PersonalICalUrl
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("ical_feed_list")
+    success_message = _("ICal feed deleted successfully")
+
+    def get_queryset(self):
+        return PersonalICalUrl.objects.filter(person=self.request.user.person)
+
+
+class ICalFeedCreateView(AdvancedCreateView):
+    model = PersonalICalUrl
+    template_name = "core/ical/ical_create.html"
+    success_url = reverse_lazy("ical_feed_list")
+    success_message = _("ICal feed created successfully")
+
+    fields = ["name", "ical_feed"]
+
+    def form_valid(self, form):
+        obj = form.save(commit=False)
+        obj.person = self.request.user.person
+        obj.save()
+        return super().form_valid(form)