diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index e1c0be7ef4f28e609c93f3b58208aa0ea4d58b3a..969cb4b22e9be1aa91f109d6a5b399cb4d4d8809 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
 
 from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget
 from dynamic_preferences.forms import PreferenceForm
+from image_cropping import ImageCropWidget
 from material import Fieldset, Layout, Row
 
 from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
@@ -309,3 +310,13 @@ class SchoolTermForm(ExtensibleForm):
     class Meta:
         model = SchoolTerm
         exclude = []
+
+class EditPersonPhotoForm(ExtensibleForm):
+    """Form to edit a persons photo."""
+
+    class Meta:
+        model = Person
+        fields = ["photo"]
+        widgets = {
+            'photo': ImageCropWidget,
+        }
diff --git a/aleksis/core/templates/core/person/edit_photo.html b/aleksis/core/templates/core/person/edit_photo.html
new file mode 100644
index 0000000000000000000000000000000000000000..17e6c79bc0384b10bb9636723748aeaa471e67a1
--- /dev/null
+++ b/aleksis/core/templates/core/person/edit_photo.html
@@ -0,0 +1,23 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n %}
+
+{% block extra_head %}
+  {{ photo_form.media }}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Edit photo{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit photo{% endblocktrans %}{% endblock %}
+
+
+{% block content %}
+
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {{ photo_form }}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/person/full.html b/aleksis/core/templates/core/person/full.html
index 80aff9b9fe0b2d3c4146929758430b3401bf5270..41bd848bc22df7308b1ef377b291633bcaf50778 100644
--- a/aleksis/core/templates/core/person/full.html
+++ b/aleksis/core/templates/core/person/full.html
@@ -22,6 +22,10 @@
           <i class="material-icons left">edit</i>
           {% trans "Edit" %}
         </a>
+        <a href="{% url 'edit_person_photo_by_id' person.id %}" class="btn waves-effect waves-light">
+          <i class="material-icons left">insert_photo</i>
+          {% trans "Edit photo" %}
+        </a>
       {% endif %}
 
       {% if can_delete_person %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 51048e5c83bdb6c540254a36754f32cdc3059591..19c4a9c5b1d9953627240b39e3eba9d290cd28b9 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -32,6 +32,7 @@ urlpatterns = [
     path("person/create", views.edit_person, name="create_person"),
     path("person/<int:id_>", views.person, name="person_by_id"),
     path("person/<int:id_>/edit", views.edit_person, name="edit_person_by_id"),
+    path("person/<int:id_>/photo/edit", views.edit_person_photo, name="edit_person_photo_by_id"),
     path("person/<int:id_>/delete", views.delete_person, name="delete_person_by_id"),
     path("groups", views.groups, name="groups"),
     path("groups/additional_fields", views.additional_fields, name="additional_fields"),
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index af2f285a7ec48c5499015c9f54388818e68d2bc1..9ab49dfcb85d95f060c89c8b02e346cec15a9ea1 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -678,3 +678,25 @@ def delete_group_type(request: HttpRequest, id_: int) -> HttpResponse:
     messages.success(request, _("The group type has been deleted."))
 
     return redirect("group_types")
+
+@permission_required("core.edit_person", fn=objectgetter_optional(Person))
+def edit_person_photo(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """Edit view for a single person, defaulting to logged-in person."""
+    context = {}
+
+    person = get_object_or_404(Person, id_)
+    context["person"] = person
+
+    # Edit form for existing group
+    edit_person_photo_form = EditPersonPhotoForm(
+        request.POST or None, request.FILES or None, instance=person
+    )
+    if request.method == "POST":
+        if edit_person_photo_form.is_valid():
+            with reversion.create_revision():
+                edit_person_photo_form.save(commit=True)
+            messages.success(request, _("The person has been saved."))
+
+    context["edit_person_photo_form"] = edit_person_photo_form
+
+    return render(request, "core/person/edit_photo.html", context)