diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index c7ab976187b4db8a7b82cb3f43a96cf3fd220517..e7c2ec5b8b0009ac879331c7827a3fcae958c25f 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -3901,39 +3901,88 @@ out_ip4_address:
 
 #define DNS_ROUTES_FWMARK_TABLE_PRIO 20053
 
+static gboolean
+_l3cfg_routed_dns_equal(GPtrArray *routes_old, GPtrArray *routes_new)
+{
+    guint i;
+
+    if (nm_g_ptr_array_len(routes_old) != nm_g_ptr_array_len(routes_new))
+        return FALSE;
+
+    if (routes_old) {
+        nm_platform_route_objs_sort(routes_old, NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY);
+    }
+
+    if (routes_new) {
+        nm_platform_route_objs_sort(routes_new, NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY);
+    }
+
+    for (i = 0; i < nm_g_ptr_array_len(routes_old); i++) {
+        if (nmp_object_cmp(routes_old->pdata[i], routes_new->pdata[i]) != 0)
+            return FALSE;
+    }
+
+    return TRUE;
+}
+
+static GPtrArray *
+_l3cfg_routed_dns_get_existing_routes(NML3Cfg *self, int addr_family)
+{
+    GPtrArray                   *routes = NULL;
+    NMPLookup                    lookup;
+    const NMDedupMultiHeadEntry *head_entry;
+    CList                       *iter;
+
+    nmp_lookup_init_object_by_ifindex(&lookup,
+                                      NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)),
+                                      self->priv.ifindex);
+
+    head_entry = nm_platform_lookup(self->priv.platform, &lookup);
+    if (!head_entry)
+        return NULL;
+
+    c_list_for_each (iter, &head_entry->lst_entries_head) {
+        const NMPObject *obj = c_list_entry(iter, NMDedupMultiEntry, lst_entries)->obj;
+
+        if (nm_platform_route_table_uncoerce(obj->ipx_route.rx.table_coerced, FALSE)
+            != DNS_ROUTES_FWMARK_TABLE_PRIO)
+            continue;
+
+        if (!routes)
+            routes = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+        g_ptr_array_add(routes, (gpointer) nmp_object_ref(obj));
+    }
+
+    return routes;
+}
+
 static void
-_l3cfg_routed_dns(NML3Cfg *self, NML3ConfigData *l3cd)
+_l3cfg_routed_dns_apply(NML3Cfg *self, const NML3ConfigData *l3cd)
 {
+    if (!l3cd)
+        return;
+
     for (int IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
-        const char *const *nameservers;
-        guint              i;
-        guint              len;
-        int                addr_family;
-        gboolean           route_added = FALSE;
+        const char *const                *nameservers;
+        guint                             i;
+        guint                             len;
+        int                               addr_family;
+        nm_auto_unref_ptrarray GPtrArray *old_routes = NULL;
+        nm_auto_unref_ptrarray GPtrArray *new_routes = NULL;
 
         addr_family = IS_IPv4 ? AF_INET : AF_INET6;
-        if (!nm_l3_config_data_get_routed_dns(l3cd, addr_family)) {
-            if (self->priv.dns_route_added_x[IS_IPv4]) {
-                /* Even if the DNS-routes feature is disabled, it was enabled
-                 * before. Therefore, we need to set one last time the routing
-                 * table sync mode to FULL, to clear the DNS routes added
-                 * previously. */
-                self->priv.dns_route_added_x[IS_IPv4] = FALSE;
-                nm_l3_config_data_set_route_table_sync(
-                    l3cd,
-                    addr_family,
-                    NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_EXCEPT_LOCAL);
-            }
-            continue;
-        }
 
-        nameservers = nm_l3_config_data_get_nameservers(l3cd, addr_family, &len);
-        nm_l3_config_data_set_route_table_sync(l3cd,
-                                               addr_family,
-                                               NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_EXCEPT_LOCAL);
+        if (!nm_l3_config_data_get_routed_dns(l3cd, addr_family))
+            goto update_routes;
+
+        _LOGT("configuring IPv%c DNS routes", nm_utils_addr_family_to_char(addr_family));
 
+        new_routes = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+
+        nameservers = nm_l3_config_data_get_nameservers(l3cd, addr_family, &len);
         for (i = 0; i < len; i++) {
             nm_auto_nmpobj NMPObject *obj = NULL;
+            NMPObject                *obj_new;
             const NMPlatformIPXRoute *route;
             NMPlatformIPXRoute        route_new;
             char                      addr_buf[INET6_ADDRSTRLEN];
@@ -3955,7 +4004,7 @@ _l3cfg_routed_dns(NML3Cfg *self, NML3ConfigData *l3cd)
                                          self->priv.ifindex,
                                          &obj);
             if (r < 0) {
-                _LOGT("could not get route to DNS %s",
+                _LOGD("could not get route to DNS server %s",
                       nm_inet_ntop(addr_family, dns.addr.addr_ptr, addr_buf));
                 continue;
             }
@@ -3964,6 +4013,7 @@ _l3cfg_routed_dns(NML3Cfg *self, NML3ConfigData *l3cd)
 
             if (IS_IPv4) {
                 route_new.r4 = (NMPlatformIP4Route) {
+                    .ifindex       = self->priv.ifindex,
                     .network       = dns.addr.addr4,
                     .plen          = 32,
                     .table_any     = FALSE,
@@ -3975,18 +4025,15 @@ _l3cfg_routed_dns(NML3Cfg *self, NML3ConfigData *l3cd)
 
                 nm_platform_ip_route_normalize(addr_family, &route_new.rx);
 
-                _LOGT("route to %s: %s",
+                _LOGT("route to DNS server %s: %s",
                       nm_inet4_ntop(dns.addr.addr4, addr_buf),
                       nm_platform_ip4_route_to_string(&route_new.r4, route_buf, sizeof(route_buf)));
 
-                nm_l3_config_data_add_route_4(l3cd, &route_new.r4);
-                nm_l3_config_data_set_route_table_sync(
-                    l3cd,
-                    AF_INET,
-                    NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_EXCEPT_LOCAL);
-                route_added = TRUE;
+                obj_new = nmp_object_new(NMP_OBJECT_TYPE_IP4_ROUTE, &route_new);
+                g_ptr_array_add(new_routes, obj_new);
             } else {
                 route_new.r6 = (NMPlatformIP6Route) {
+                    .ifindex       = self->priv.ifindex,
                     .network       = dns.addr.addr6,
                     .plen          = 128,
                     .table_any     = FALSE,
@@ -3998,22 +4045,23 @@ _l3cfg_routed_dns(NML3Cfg *self, NML3ConfigData *l3cd)
 
                 nm_platform_ip_route_normalize(addr_family, &route_new.rx);
 
-                _LOGT("route to %s: %s",
+                _LOGT("route to DNS server %s: %s",
                       nm_inet6_ntop(&dns.addr.addr6, addr_buf),
                       nm_platform_ip6_route_to_string(&route_new.r6, route_buf, sizeof(route_buf)));
 
-                nm_l3_config_data_add_route_6(l3cd, &route_new.r6);
-                route_added = TRUE;
+                obj_new = nmp_object_new(NMP_OBJECT_TYPE_IP6_ROUTE, &route_new);
+                g_ptr_array_add(new_routes, obj_new);
             }
         }
 
-        if (route_added) {
+        if (new_routes->len > 0) {
             NMPlatformRoutingRule rule;
+            NMPObject             rule_obj;
 
             /* Add a routing rule that selects the table when not using the
              * special fwmark. Note that the rule is shared between all
-             * devices that use DNS routes. At the moment there is no cleanup
-             * mechanism: once added the rule stays forever. */
+             * devices that use DNS routes. There is no cleanup mechanism:
+             * once added the rule stays forever. */
             rule = ((NMPlatformRoutingRule) {
                 .addr_family = addr_family,
                 .flags       = FIB_RULE_INVERT,
@@ -4025,12 +4073,39 @@ _l3cfg_routed_dns(NML3Cfg *self, NML3ConfigData *l3cd)
                 .protocol    = RTPROT_STATIC,
             });
 
-            /* FIXME: don't add the rule every time */
-            nmp_global_tracker_track_rule(self->priv.global_tracker, &rule, 10, self, NULL);
-            nmp_global_tracker_sync(self->priv.global_tracker, NMP_OBJECT_TYPE_ROUTING_RULE, TRUE);
+            nmp_object_stackinit(&rule_obj, NMP_OBJECT_TYPE_ROUTING_RULE, &rule);
+
+            if (!nm_platform_lookup_obj(self->priv.platform,
+                                        NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+                                        &rule_obj)) {
+                _LOGT("adding rule to DNS routing table");
+                nmp_global_tracker_track_rule(self->priv.global_tracker, &rule, 10, self, NULL);
+                nmp_global_tracker_sync(self->priv.global_tracker,
+                                        NMP_OBJECT_TYPE_ROUTING_RULE,
+                                        TRUE);
+            }
         }
 
-        self->priv.dns_route_added_x[IS_IPv4] = route_added;
+update_routes:
+        old_routes = _l3cfg_routed_dns_get_existing_routes(self, addr_family);
+
+        if (!_l3cfg_routed_dns_equal(old_routes, new_routes)) {
+            if (old_routes) {
+                _LOGT("deleting old DNS routes");
+                for (i = 0; i < old_routes->len; i++) {
+                    nm_platform_object_delete(self->priv.platform, old_routes->pdata[i]);
+                }
+            }
+            if (new_routes) {
+                _LOGT("adding new DNS routes");
+                for (i = 0; i < new_routes->len; i++) {
+                    nm_platform_ip_route_add(self->priv.platform,
+                                             NMP_NLM_FLAG_REPLACE,
+                                             new_routes->pdata[i],
+                                             NULL);
+                }
+            }
+        }
     }
 }
 
@@ -4201,8 +4276,6 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
         nm_assert(l3cd);
         nm_assert(nm_l3_config_data_get_ifindex(l3cd) == self->priv.ifindex);
 
-        _l3cfg_routed_dns(self, l3cd);
-
         nm_l3_config_data_seal(l3cd);
     }
 
@@ -5326,6 +5399,8 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
     _l3_commit_one(self, AF_INET, commit_type, l3cd_old);
     _l3_commit_one(self, AF_INET6, commit_type, l3cd_old);
 
+    _l3cfg_routed_dns_apply(self, self->priv.p->combined_l3cd_commited);
+
     _failedobj_reschedule(self, 0);
 
     _l3_commit_mptcp(self, commit_type);
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index a352860d4c350421c42db58670b315ad4d9743aa..8581f6c45eab6577cf3003eceb049dc13d5a7cfa 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -198,7 +198,6 @@ struct _NML3Cfg {
         const NMPObject          *plobj;
         const NMPObject          *plobj_next;
         int                       ifindex;
-        gboolean                  dns_route_added_x[2]; /* index with IS_IPv4 */
     } priv;
 
     /* NML3Cfg strongly cooperates with NMNetns. The latter is