diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4022882bcbd013fb6bee4f38c3ed3d3574dc92a2..e2801658f04b6b17666e5194debb0a8e0d7c1d61 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -17,6 +17,9 @@ Changed
 Fixed
 ~~~~~
 
+* No person was created and linked to the PersonInvitation object when invite by e-mail is used
+* No valid data in the second e-mail field of the signup form when it was disabled
+* Invitation options were displayed even when the feature was disabled
 * Inviting newly created persons for registration failed
 * [Docker] Do not clear cache in migration container die to session invalidation issues
 * Notification email about user changes was broken
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 58bbba7f63869e1212c50ecd7131fdcd4e72999a..ef4184812f73d22bb1dcfa1ff84f74f722c3c774 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -18,6 +18,7 @@ from dj_cleavejs import CleaveWidget
 from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
 from dynamic_preferences.forms import PreferenceForm
 from guardian.shortcuts import assign_perm
+from invitations.forms import InviteForm
 from material import Fieldset, Layout, Row
 
 from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
@@ -29,6 +30,7 @@ from .models import (
     GroupType,
     OAuthApplication,
     Person,
+    PersonInvitation,
     SchoolTerm,
 )
 from .registries import (
@@ -410,6 +412,31 @@ class InvitationCodeForm(forms.Form):
         self.fields["code"].widget = CleaveWidget(blocks=blocks, delimiter="-", uppercase=True)
 
 
+class PersonCreateInviteForm(InviteForm):
+    """Custom form to create a person and invite them."""
+
+    first_name = forms.CharField(label=_("First name"), required=True)
+    last_name = forms.CharField(label=_("Last name"), required=True)
+
+    layout = Layout(
+        Row("first_name", "last_name"),
+        Row("email"),
+    )
+
+    def clean_email(self):
+        if Person.objects.filter(email=self.cleaned_data["email"]).exists():
+            raise ValidationError(_("A person is using this e-mail address"))
+        return super().clean_email()
+
+    def save(self, email):
+        person = Person.objects.create(
+            first_name=self.cleaned_data["first_name"],
+            last_name=self.cleaned_data["last_name"],
+            email=email,
+        )
+        return PersonInvitation.create(email=email, person=person)
+
+
 class SelectPermissionForm(forms.Form):
     """Select a permission to assign."""
 
@@ -597,6 +624,7 @@ class AccountRegisterForm(SignupForm, ExtensibleForm):
 
             if person:
                 available_fields = [field.name for field in Person._meta.get_fields()]
+                self.fields["email2"].initial = person.email
                 for field in self.fields:
                     if field in available_fields and getattr(person, field):
                         self.fields[field].disabled = True
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index d8f63e606073f92e8db799206012cfc9a998a5d8..812a9d254ca4bff2e9372ed7969d0cb2b811c176 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1150,8 +1150,16 @@ class PersonInvitation(AbstractBaseInvitation, PureDjangoModel):
     def __str__(self) -> str:
         return f"{self.email} ({self.inviter})"
 
-    key_expired = Invitation.key_expired
+    @classmethod
+    def create(cls, email, inviter=None, **kwargs):
+        length = get_site_preferences()["auth__invite_code_length"]
+        packet_size = get_site_preferences()["auth__invite_code_packet_size"]
+        code = generate_random_code(length, packet_size)
 
+        instance = cls.objects.create(email=email, inviter=inviter, key=code, **kwargs)
+        return instance
+
+    key_expired = Invitation.key_expired
     send_invitation = Invitation.send_invitation
 
 
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index ad1f55fcc44f470fac11c362c456451a56620785..022fa11a00671a3acb0775c2229a378bd3981342 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -329,7 +329,7 @@ rules.add_perm("core.can_change_password", can_change_password_predicate)
 invite_enabled_predicate = is_site_preference_set(section="auth", pref="invite_enabled")
 rules.add_perm("core.invite_enabled", invite_enabled_predicate)
 
-can_invite_predicate = has_person & has_global_perm("core.invite")
+can_invite_predicate = invite_enabled_predicate & has_person & has_global_perm("core.invite")
 rules.add_perm("core.can_invite", can_invite_predicate)
 
 # OAuth2 permissions
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 22d77d8c8baf42c823d8c8efc8d13a2039270e8a..087762de88b667f90c942f3596a7df4991faf627 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -372,6 +372,8 @@ INVITATIONS_INVITATION_EXPIRY = _settings.get("auth.invitation.expiry", 3)
 INVITATIONS_EMAIL_SUBJECT_PREFIX = ACCOUNT_EMAIL_SUBJECT_PREFIX
 # Use custom invitation model
 INVITATIONS_INVITATION_MODEL = "core.PersonInvitation"
+# Use custom invitation form
+INVITATIONS_INVITE_FORM = "aleksis.core.forms.PersonCreateInviteForm"
 # Display error message if invitation code is invalid
 INVITATIONS_GONE_ON_ACCEPT_ERROR = False
 # Mark invitation as accepted after signup
diff --git a/aleksis/core/templates/core/person/full.html b/aleksis/core/templates/core/person/full.html
index 24e267db3caf7b6a9886f10269a4db654514f696..23f8ba29b30188dd4cdf5b76af23991a0f618119 100644
--- a/aleksis/core/templates/core/person/full.html
+++ b/aleksis/core/templates/core/person/full.html
@@ -48,7 +48,7 @@
           </a>
         {% endif %}
 
-        {% if can_invite and not person.user %}
+        {% if invite_enabled and can_invite and not person.user %}
           <a href="{% url "invite_person_by_id" person.id %}" class="btn waves-effect waves-light">
             <i class="material-icons left">card_giftcard</i>
             {% trans "Invite user" %}
diff --git a/aleksis/core/templates/invitations/disabled.html b/aleksis/core/templates/invitations/disabled.html
new file mode 100644
index 0000000000000000000000000000000000000000..2515a01f2437c7f617cc14fcee0226bedeacd341
--- /dev/null
+++ b/aleksis/core/templates/invitations/disabled.html
@@ -0,0 +1,21 @@
+{% extends "core/base.html" %}
+
+{% load i18n %}
+
+{% block browser_title %}{% blocktrans %}The invite feature is disabled{% endblocktrans %}{% endblock %}
+{% block no_page_title %}{% endblock %}
+
+{% block content %}
+  <div class="container">
+    <div class="card red">
+      <div class="card-content white-text">
+        <i class="material-icons small left">disabled_by_default</i>
+        <span class="card-title">{% blocktrans %}The invite feature is disabled.{% endblocktrans %}</span>
+        <p>
+          {% trans "To enable it, switch on the corresponding checkbox in the authentication section of the " %}
+          <a href="{% url "preferences_site" %}">{% trans "site preferences page" %}</a>.
+        </p>
+      </div>
+    </div>
+  </div>
+{% endblock %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 20c6bad6174aa27f619828366ac5d937faa17bd3..c91bae7f29c978d30e96361f92213eec7c5679d1 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -40,6 +40,7 @@ urlpatterns = [
         views.GenerateInvitationCode.as_view(),
         name="generate_invitation_code",
     ),
+    path("invitations/disabled", views.InviteDisabledView.as_view(), name="invite_disabled"),
     path("invitations/", include("invitations.urls")),
     path(
         "accounts/social/connections/<int:pk>/delete",
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index c3100c7bf789c3e4ed12d03a20f8e34ae8adb3b3..fc32d75982a26e9fafbcd0f4a98e2b0569e585c4 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -330,6 +330,9 @@ def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     # Get groups where person is member of
     context["groups"] = person.member_of.all()
 
+    # Get whether the invitation feature is enabled in the preferences
+    context["invite_enabled"] = get_site_preferences()["auth__invite_enabled"]
+
     return render(request, "core/person/full.html", context)
 
 
@@ -1075,6 +1078,11 @@ class InvitePerson(PermissionRequiredMixin, SingleTableView, SendInvite):
     table_class = InvitationsTable
     context = {}
 
+    def dispatch(self, request, *args, **kwargs):
+        if not get_site_preferences()["auth__invite_enabled"]:
+            return HttpResponseRedirect(reverse_lazy("invite_disabled"))
+        return super().dispatch(request, *args, **kwargs)
+
     # Get queryset of invitations
     def get_context_data(self, **kwargs):
         queryset = kwargs.pop("object_list", None)
@@ -1102,6 +1110,7 @@ class EnterInvitationCode(FormView):
             accept_invitation(
                 invitation=invitation, request=self.request, signal_sender=self.request.user
             )
+            self.request.session["invitation_code_entered"] = True
             return redirect("account_signup")
         return redirect("invitations:accept-invite", code)
 
@@ -1409,8 +1418,10 @@ class AccountRegisterView(SignupView):
     success_url = reverse_lazy("index")
 
     def dispatch(self, request, *args, **kwargs):
-        if not request.user.has_perm("core.can_register") and not request.session.get(
-            "account_verified_email"
+        if (
+            not request.user.has_perm("core.can_register")
+            and not request.session.get("account_verified_email")
+            and not request.session.get("invitation_code_entered")
         ):
             raise PermissionDenied()
         return super(AccountRegisterView, self).dispatch(request, *args, **kwargs)
@@ -1421,10 +1432,17 @@ class AccountRegisterView(SignupView):
         return kwargs
 
 
-class InvitePersonByID(SingleObjectMixin, View):
+class InvitePersonByID(PermissionRequiredMixin, SingleObjectMixin, View):
     """Custom view to invite person by their ID."""
 
     model = Person
+    success_url = reverse_lazy("persons")
+    permission_required = "core.can_invite"
+
+    def dispatch(self, request, *args, **kwargs):
+        if not get_site_preferences()["auth__invite_enabled"]:
+            return HttpResponseRedirect(reverse_lazy("invite_disabled"))
+        return super().dispatch(request, *args, **kwargs)
 
     def get(self, request, *args, **kwargs):
         person = self.get_object()
@@ -1447,6 +1465,18 @@ class InvitePersonByID(SingleObjectMixin, View):
         return HttpResponseRedirect(person.get_absolute_url())
 
 
+class InviteDisabledView(PermissionRequiredMixin, TemplateView):
+    """View to display a notice that the invite feature is disabled and how to enable it."""
+
+    template_name = "invitations/disabled.html"
+    permission_required = "core.change_site_preferences_rule"
+
+    def dispatch(self, request, *args, **kwargs):
+        if get_site_preferences()["auth__invite_enabled"]:
+            raise PermissionDenied()
+        return super().dispatch(request, *args, **kwargs)
+
+
 class LoginView(AllAuthLoginView):
     """Custom login view covering e-mail verification if mandatory.