This not a complete and ready-made answer, but the below should get you started.

First thing to note is, deleting and modifying are different functionalities, so different functions and hooks are relevant to it. Namely we are talking about wp_delete_term() - source and wp_update_term() - source - regarding the functions.

So lets take a look at wp_delete_term(), you have to at least consider two hooks: edit_term_taxonomies and delete_term_taxonomy. They are both relevant because of the bulk action and because of the handling of hierarchical taxonomies.

Below some exemplary code how to approach this. I've tried to explain the most important things inside the code. The rest you have to read up on yourself.

First the edit_term_taxonomies hook:

add_action(
    'edit_term_taxonomies',
    'wpse160191_top_level_lock_down_edit_term_taxonomies',
    10,
    1
);
function wpse160191_top_level_lock_down_edit_term_taxonomies(
    $edit_tt_ids
) {
    $screen_obj = get_current_screen();
    $base = $screen_obj-&gt;base;
    $taxonomy = $screen_obj-&gt;taxonomy;

    if(
        // these are the conditions we are checking against
        // is this a tag edit page
        $base == 'edit-tags'
        // we want this only for a specific taxonomy
        &amp;&amp; $taxonomy == 'category'
        // not super and/or admin
        &amp;&amp; ! current_user_can( 'manage_options' )
        // a least an editor
        &amp;&amp; current_user_can( 'manage_categories' )
    ) {

        // these are the terms up for deletion
        $terms_queue_for_deletion = $_POST['delete_tags'];
        $term_parents_ids = array();
        // we need information if or not one of the terms is a top level one
        foreach ( $terms_queue_for_deletion as $term_id ) {
            $term_obj = get_term_by( 'id', $term_id, 'category' );
            $term_parent = $term_obj-&gt;parent;
            $term_parents_ids[] = $term_parent;
        }

        if( empty( $edit_tt_ids ) ) {
            return;
        }

        if( ! empty( $edit_tt_ids ) ) {
            if ( in_array( 0, $term_parents_ids ) ) {
                $msg = new WP_Error(
                'parent_terms_are_forever',
                __('Sorry, you are not allowed to delete top level terms.')
                );
                wp_die( $msg );
            } else {
                return;
            }
        }
    }
}


Second the delete_term_taxonomy hook:

add_action(
    'delete_term_taxonomy',
    'wpse160191_top_level_lock_down_delete_term_taxonomy',
    10,
    1
);
function wpse160191_top_level_lock_down_delete_term_taxonomy(
    $tt_id
) {

    $screen_obj = get_current_screen();
    $base = $screen_obj-&gt;base;
    $taxonomy = $screen_obj-&gt;taxonomy;

    if (
        // these are the conditions we are checking against
        // is this a tag edit page
        $base == 'edit-tags'
        // we want this only for a specific taxonomy
        &amp;&amp; $taxonomy == 'category'
        // not super and/or admin
        &amp;&amp; ! current_user_can( 'manage_options' )
        // a least an editor
        &amp;&amp; current_user_can( 'manage_categories' )
    ) {
        $term_obj = get_term_by( 'term_taxonomy_id', $tt_id, 'category' );
        $parent_id = $term_obj-&gt;parent;
        if (
            // this applies only to top level terms
            $parent_id == 0
        ) {
            $msg = new WP_Error(
                'parent_terms_are_forever',
                __('s:Sorry, you are not allowed to delete top level terms.')
            );
            wp_die( $msg );
        }
    }
}


As mentioned above, it gets a bit more complicated, mainly because of the AJAX actions. If you are thinking about doing this with the well known and simple check:

if ( defined('DOING_AJAX') &amp;&amp; DOING_AJAX ) {
    // code
}


I can tell you right now, that I'm pretty certain it won't be sufficient enough. Mostly because you won't be able to do all the checks you need. If you are fine with just disabling or, to be exact, removing the link to the AJAX action, you can do it somewhat like this:

add_action(
    'admin_head' ,
    'wpse160191_remove_ajax_delete_link'
);
function wpse160191_remove_ajax_delete_link() {
    $screen_obj = get_current_screen();
    $base = $screen_obj-&gt;base;
    $taxonomy = $screen_obj-&gt;taxonomy;

    if(
        // lets just remove the link on that one page and not everywhere
        $base == 'edit-tags'
        &amp;&amp; $taxonomy == 'category'
    ) {
    ?&gt;
        &lt;script type="text/javascript"&gt;
            jQuery(document).ready(function($) {
                var element = 'span.delete';
                $(element).each(function(){
                    $(element).css('opacity', '0');
                    $(element).css('pointer-events', 'none');
                    $(element).remove();
                });
            });
        &lt;/script&gt;
    &lt;?php
    }
}


Of course you shouldn't do it inline and so on, but you can optimize yourself. This of course is not very satisfying, because we just removed the functionality.

One way I imagine to deal with the AJAX action would be to make use of debug_backtrace(). This would offer a way to determine and handle the AJAX action correctly. It could be implemented into the above shown code. A caveat of course is that debug_backtrace() is performance-wise expensive and in a larger scale surely not recommendable.
Another way would be to implement your own AJAX action as replacement for wp_ajax_delete_tag(), but to do so you have to additionally either have to extend the WP_Terms_List_Table or use some hooks in there to achieve this. Not exactly sure at the moment, so you have to inspect that further yourself.

The procedure for wp_update_term() and disallowing updating/modifying is analogous to the above shown. With the same considerations concerning the AJAX part, especially because of the inline quick edit possibility. 



At first I totally misread the question and gave this as an answer.
Keeping it because it might be useful to others.

What you can do is hooking into the pre_insert_term to do the checks needed.

Code:

add_action( 'pre_insert_term', 'wpse160191_restrict_top_level_term_creation', 10, 2 );
function wpse160191_restrict_top_level_term_creation( $term, $taxonomy ) {
    if( 
        // these are the conditions we are checking against
        // we want this only for a specific taxonomy
        $taxonomy == 'topics'
        // a value of 0 or -1 does indicate the top level
        &amp;&amp; $_POST[ 'parent' ] &lt;= '0'
        // not super and/or admin
        &amp;&amp; ! current_user_can( 'manage_options' )
        // a least an editor
        &amp;&amp; current_user_can( 'manage_categories' )
    ) {
        // the user is an editor and not allowed to add parent terms
        // so return a friendly message so the user at least knows 
        return new WP_Error(
            'parent_terms_not_allowed_for_editors',
            __('Sorry, you are not allowed to add parent terms.')
        );
    } else {
        // a super and/or admin, s|he is allowed to do what s|he wants
        return $term;
    }
}


The pre_insert_term hook is part of the wp_insert_term() function.


Some Notes:


The parent value of the $args is 0, when no parent is selected. But we have no access to those inside the action, the form does return the value of -1 for $_POST[ 'parent' ] if »None« is selected. So &lt;= '0' should work in any case.  
We are not passing the user role to the current_user_can() function, because that is just wrong - although you see it a lot, it still is bad. Instead capabilities are used to determine, which kind of user we are dealing with.


Can I lock down custom taxonomies on a parent term level, but not a child term?
To formulate this question precisely, I will define an idealized hypothetical "perfect" hash function $H(n)$ which has nice scalability properties, and will formulate a problem PERFECT HASHCASH in terms of that, understanding that practical considerations may end up yielding only an approximation of this ideal.

To keep it simple, we will say that our hash function $H(n)$ takes as input a single natural number $n$. Then we say that $H(n)$ is a perfect hash function iff:


$H:\mathbb N \to \{0,1\}^\infty$; That is, $H$ maps each natural number to an infinite binary sequence, and which the time complexity to compute any initial segment $s$ is polynomial in the size of $n$ and $s$, (making it a sponge function).
For any initial segment of length $d$, the set of all natural numbers $n$ such that $H(n)$ shares that initial segment has natural density $2^{-d}$.


The first thing formalizes the scalability of our function, and the second thing formalizes the idea that we want all hashes to appear roughly "equally often" as an output. Other than that, our perfect hash function is a black box, and we don't care much about exactly how it works, so long as it meets the above properties, as well as the usual desiderata applying to hash functions (easy to compute, hard to invert, hard to find collisions, etc).

Predicated on the assumption that a perfect hash function exists, we can now define the problem PERFECT HASHCASH as follows: PERFECT HASHCASH takes as input a perfect hash function $H$, a natural number $n$, and the zero vector $0^d$ of length $d$, which can be thought of as a unary representation of $d$. A solution to PERFECT HASHCASH consists of an $n$ and $d$ such that $H(n)$ starts with $0^d$.

Given those inputs, it is clear that PERFECT HASHCASH is in the complexity class TFNP, since this is a function problem and a solution is guaranteed to exist.

Can we also identify PERFECT HASHCASH as a member of any complexity class finer than TFNP?

Could it perhaps be in PPP? PPA? PPAD? Something else?

For background, see Complexity class on Wikipedia.
